
package toponet.overlay;

import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Vector;

import toponet.geometry.*;
import sicsim.network.core.Bandwidth;
import sicsim.network.core.FailureDetector;
import sicsim.network.core.Monitor;
import sicsim.network.core.OverlayNetwork;
import sicsim.network.links.AbstractLink;
import sicsim.network.peers.AbstractPeer;
import sicsim.network.peers.PeerEventListener;
import sicsim.types.Message;
import sicsim.types.NodeId;

public class ToponetPeer extends AbstractPeer {
	public static int count = 0;
	private Point location;
	private FingerTable ft;
	//For the mobule nodes
	private Hashtable <NodeId,FingerEntry> mobiles;
	
	// Initialize the node an
	public void init(NodeId nodeId, AbstractLink link, Bandwidth bandwidth, FailureDetector failureDetector, OverlayNetwork overlay, Monitor monitor) {
		super.init(nodeId, link, bandwidth, failureDetector, overlay, monitor);
		this.ft = new FingerTable(this);
		this.location = this.getRandomLocation();
		System.out.println("Peer = "+(++count)+" nid="+nodeId+" initializing.");
		this.mobiles = new Hashtable<NodeId, FingerEntry>();
	}
	
	public void setFingerTable(FingerTable ft) {
		this.ft = ft;
	}
	
	// not neccessary
	public void setLoction(double lon, double lat) {
		this.location = new Point(lon,lat);
	}
	
	@Override
	public void create(long arg0) {
		//just add the first node to the overlay
		//do we have to use a loopback message here? 
		this.overlay.add(this.nodeId);
	}

	@Override
	public void failure(NodeId arg0, long arg1) {
		// TODO Auto-generated method stub
	}

	@Override
	public void join(long arg0) {
		//find a random node and send a join message
		NodeId randomNode = this.overlay.getRandomNodeIdFromNetwork();
		
		if (randomNode != null) { 
			//for the time being, a peer's coordinates are a string, might have to change the message class later. 
			if (this.inConvexHull()) {
				//if it's an outer node
				this.sendMsg(randomNode, new Message("OUTERJOIN", this.toString()));
			
			}
			else {
				//else normal join
				this.sendMsg(randomNode, new Message("JOIN", this.toString()));		
			}
		}
		
		//add yourself to the overlay, but the finger table is going to be done after the random node responds 
		//do we have to use a loopback message to check again? 
		this.overlay.add(this.nodeId);
	}	
	
	@Override
	public void leave(long arg0) {
		if(this.inConvexHull()) {
			// If node is in convex hull then get the rightest element
			// and begin calculating the lower hull. All nodes of the lower hull
			// must be connected to each other.
		}
		else {
			// ALL THESE IF NODE IS NOT IN CONVEX HULL
			int critical = this.ft.getFingerEntries().size()/3;
			NodeId temp;
			Point tempLoc;
			NodeId nid1 = this.ft.getFingerEntries().elementAt(0).getNodeId();
			Point loc1 = this.ft.getFingerEntries().elementAt(0).getPoint();
			NodeId nid2,nid3;
			Point loc2, loc3;
			if(this.ft.getFingerEntries().size()%3 == 2) {
				critical++;
				nid2 = this.ft.getFingerEntries().elementAt(critical).getNodeId();
				loc2 = this.ft.getFingerEntries().elementAt(critical).getPoint();
				nid3 = this.ft.getFingerEntries().elementAt((critical)*2).getNodeId();
				loc3 = this.ft.getFingerEntries().elementAt((critical)*2).getPoint();
			}
			else {
				nid2 = this.ft.getFingerEntries().elementAt(critical).getNodeId();
				loc2 = this.ft.getFingerEntries().elementAt(critical).getPoint();
				nid3 = this.ft.getFingerEntries().elementAt(critical*2).getNodeId();
				loc3 = this.ft.getFingerEntries().elementAt(critical*2).getPoint();
			}
			Enumeration<FingerEntry> e = this.ft.getFingerEntries().elements();
			int i=0;
			while(e.hasMoreElements()) {
				NodeId nid = e.nextElement().getNodeId();
				if(nid.equals(nid1)) {	
					System.out.println("Send message FTADD to "+nid2+" to add "+nid1);
					System.out.println("Send message FTADD to "+nid3+" to add "+nid1);
					System.out.println("Send message FTDELME to "+nid1);
				}
				if(nid.equals(nid2)) {
					System.out.println("Send message FTADD to "+nid1+" to add "+nid2);
					System.out.println("Send message FTADD to "+nid3+" to add "+nid2);
					System.out.println("Send message FTDELME to "+nid2);
				}
				if(nid.equals(nid3)) {
					System.out.println("Send message FTADD to "+nid1+" to add "+nid3);
					System.out.println("Send message FTADD to "+nid2+" to add "+nid3);
					System.out.println("Send message FTDELME to "+nid3);
				}
				if(i>1 && i<critical) {
					temp = this.ft.getFingerEntries().elementAt(i).getNodeId();
					System.out.println("Send message FTADD to "+nid1+" to add "+temp);
					System.out.println("Send message FTDELME to "+temp);
				}
				if(i>(critical+1) && i<(critical*2)) {
					temp = this.ft.getFingerEntries().elementAt(i).getNodeId();
					System.out.println("Send message FTADD to "+nid2+" to add "+temp);
					System.out.println("Send message FTDELME to "+temp);
				}
				if(i>(critical*2+1)) {
					temp = this.ft.getFingerEntries().elementAt(i).getNodeId();
					System.out.println("Send message FTADD to "+nid3+" to add "+temp);
					System.out.println("Send message FTDELME to "+temp);
				}
				i++;
			}
		}
	}

	@Override
	public synchronized void receive(NodeId srcId, Message data, long currentTime) {
		if (this.listeners.containsKey(data.type))
			this.listeners.get(data.type).receivedEvent(srcId, data);
		else
			System.out.println("PEER: I'm node " + this.nodeId + " and event " + data.type + " is not registered!");
	}
	
	public void registerEvents() {
		
		this.addEventListener("JOIN", new PeerEventListener() {
			public void receivedEvent(NodeId srcId, Message msg) {
				handleJoinEvent(srcId, msg);
			}
		});
		this.addEventListener("FTADD", new PeerEventListener() {
			public void receivedEvent(NodeId srcId, Message msg) {
				handleFTAddEvent(srcId, msg);
			}
		});
		// Send an empty message to delete the source node from the finger table.
		this.addEventListener("FTDELME", new PeerEventListener() {
			public void receivedEvent(NodeId srcId, Message msg) {
				handleFTDelEvent(srcId, msg);
			}
		});
		this.addEventListener("OUTERJOIN", new PeerEventListener() {
			public void receivedEvent(NodeId srcId, Message msg) {
				handleOuterJoinEvent(srcId, msg);
			}
		});
		this.addEventListener("OUTERCHECK", new PeerEventListener() {
			public void receivedEvent(NodeId srcId, Message msg) {
				handleOuterCheckEvent(srcId, msg);
			}
		});
		
		this.addEventListener("MOBILOCASK", new PeerEventListener() {
			public void receivedEvent(NodeId srcId, Message msg) {
				handleMobileLocAskEvent(srcId, msg);
			}
		});
		this.addEventListener("MOBIJOIN", new PeerEventListener() {
			public void receivedEvent(NodeId srcId, Message msg) {
				handleMobileJoinEvent(srcId, msg);
			}
		});
		this.addEventListener("MOBIDELME", new PeerEventListener() {
			public void receivedEvent(NodeId srcId, Message msg) {
				handleMobileDeleteEvent(srcId, msg);
			}
		});
	}

	private void handleJoinEvent(NodeId srcId, Message msg) {
		//get the source's coordinates
		Point src = new Point(msg.data.split("&")); 
		
		//check if the source has the same coordinates with the current peer
		if (src.equals(this.getLocation()))
			return; 
	
		//check if node is inside any of the peer's triangles	
		//maybe the FT class should be doing these things instead of the peer?
		Triangle[] temp = this.ft.getTriangles(); 

		if (temp != null) { 
			//here we've got at least one formed triangle
			System.out.println("HERE I AM");
			for (int i = 0; i < temp.length; i++) { 
				if (temp[i] != null && temp[i].containsPoint(src)) {
					//needs to be tested: do multiple messages work?  
					
					//fix FT's 
					NodeId[] split = this.ft.getVertice(i);
					
					this.sendMsg(split[0], new Message("FTADD", src.toString() + "&" + srcId.toString()));
					this.sendMsg(split[1], new Message("FTADD", src.toString() + "&" + srcId.toString()));
					this.loopback(new Message("FTADD", src.toString() + "&" + srcId.toString()), 0);
					//this.ft.addEntry(srcId, src);

					//send message back to the source node to update his FT entry
					//this.sendMsg(srcId, new Message("FTADD", temp[i].getPoint(true).toString() + "&" + split[0].toString()));
					//this.sendMsg(srcId, new Message("FTADD", temp[i].getPoint(false).toString() + "&" + split[1].toString()));
					//this.sendMsg(srcId, new Message("FTADD", this.location.toString() + "&" + this.nodeId.toString()));
					break;
				}
			}
			
			//if everything else fails, check the closest node to the source and forward the request 
			this.sendMsg(this.ft.getClosest(src), msg);
		}
		else if (this.ft.getFingerEntries().size() > 1) {
			//  no triangles are formed
			
			//send message to the first FT entry and back to the source 
			this.sendMsg(this.ft.getNodeId(0), new Message("FTADD", src.toString() + "&" + srcId.toString()));
			//this.sendMsg(srcId, new Message("FTADD", this.ft.getPoint(0).toString() + "&" + this.ft.getNodeId(0).toString()));
			
			//if there is a second entry, send requests to that peer, too.
			if (this.ft.getFingerEntries().size() == 2) {
				this.sendMsg(this.ft.getNodeId(1), new Message("FTADD", src.toString() + "&" + srcId.toString()));
				//this.sendMsg(srcId, new Message("FTADD", this.ft.getPoint(1).toString() + "&" + this.ft.getNodeId(1).toString()));		
			}
		}
		else {
			//no triangles formed and FT empty. 
			//send message with your own characteristics
			this.sendMsg(this.getId(), new Message("FTADD", src.toString() + "&" + srcId.toString()));
			//this.sendMsg(srcId, new Message("FTADD", this.toString() + "&" + this.getId().toString()));			
		}
	}
	
	private void handleOuterJoinEvent(NodeId srcId, Message msg) {
		//get the source's coordinates
		Point src = new Point(msg.data.split("&")); 
		
		//check if the source has the same coordinates with the current peer
		if (src.equals(this.getLocation()))
			return; 
	
		//get a node as close as possible to the source
		NodeId temp = this.ft.getClosest(src);
		
		if (temp != null) {
			this.sendMsg(temp, msg);
			return;
		}
	}	
	
	private void handleOuterCheckEvent(NodeId srcId, Message msg) {
		//get the source's coordinates
		Point src = new Point(msg.data.split("&")); 
		
		//add the node to your FT. 
		this.loopback(new Message("FTADD", src.toString() + "&" + srcId.toString()), 0);

		//get the first and the last nodes from your FT
		NodeId first = this.ft.getNodeId(0);
		Point firstP = this.ft.getPoint(0);
		NodeId last  = this.ft.getNodeId(this.ft.getFingerEntries().size()-1);
		Point lastP  = this.ft.getPoint(this.ft.getFingerEntries().size()-1);
		
		if (Orientation.isCounterClockwise(this.location.getX(), this.location.getY(), src.getX(), src.getY(), firstP.getX(), firstP.getY())) { 
			this.sendMsg(first, new Message("OUTERCHECK", src.toString() + "&" + srcId.toString()));		
		} 
		else if (Orientation.isCounterClockwise(this.location.getX(), this.location.getY(), src.getX(), src.getY(), lastP.getX(), lastP.getY())) {
			this.sendMsg(last, new Message("OUTERCHECK", src.toString() + "&" + srcId.toString()));
		}	
	}

	private void handleFTAddEvent(NodeId srcId, Message msg) {
		//message is in this format: lan&lon&id@ip
		//splits the message in [[lan], [lon], [id@ip]]
		String[] parse = msg.data.split("&");
		//splits the last parse entry to [[id], [ip]]
		String[] node = parse[2].split("@");
		//reconstructs the point by using lan and lon
		Point p0 = new Point(parse);
		//reconstructs the NodeId by using id and ip
		NodeId n0 = new NodeId(Integer.parseInt(node[0]), Integer.parseInt(node[1]));
		//add the new entry to the FT 
		if (this.ft.addEntry(n0, p0)) {
			System.out.println(this.nodeId + " adds to his FT " + n0 + " request sent from " + srcId);
			this.sendMsg(srcId, new Message("FTADD", this.location.toString() + "&" + this.nodeId.toString()));
		}
	}
	
	private void handleFTDelEvent(NodeId srcId, Message msg) {
		// Not implemented for leaving node in the convex hull
		this.ft.deleteEntry(srcId);
	}

	private void handleMobileLocAskEvent(NodeId srcId, Message msg) {
		// TODO Auto-generated method stub
		//System.out.println("Node "+srcId+" requests to be connected.");
		String [] loc = new String[2];
		loc = msg.data.split("&");
		Point nnodeloc = new Point(Double.parseDouble(loc[0]),Double.parseDouble(loc[1]));
		Triangle[] temp = this.ft.getTriangles();
		if (temp != null) { 
			for (int i = 0; i < temp.length; i++) { 
				if (temp[i] != null && temp[i].containsPoint(nnodeloc)) {
					System.out.println("I am "+this.nodeId+" and i have you.");
					this.sendMsg(srcId, new Message("LOCRESP", ""+this.location.euclideanDist(nnodeloc)));
				}
			}
		}
		else {
			//System.out.println("I am "+this.nodeId+" and i have you. NO TRIANGLES");	
			this.sendMsg(srcId, new Message("LOCRESP", ""+this.location.euclideanDist(nnodeloc)));
		}
	}
	
	private void handleMobileJoinEvent(NodeId srcId, Message msg) {
		String [] loc = new String[2];
		loc = msg.data.split("&");
		Point nnodeloc = new Point(Double.parseDouble(loc[0]),Double.parseDouble(loc[1]));
		this.mobiles.put(srcId,new FingerEntry(srcId, nnodeloc));
	}
	
	private void handleMobileDeleteEvent(NodeId srcId, Message msg) {
		this.mobiles.remove(srcId);
	}

	@Override
	public void restore(String arg0) {
		// TODO Auto-generated method stub
		
	}

	@Override
	public void signal(int arg0, long arg1) {
		// TODO Auto-generated method stub
		
	}

	@Override
	public void syncMethod(long arg0) {
		// TODO Auto-generated method stub
		
	}

	@Override
	public String toString() {
		return this.location.toString();
	}
	
	public Point getLocation() {
		return this.location;
	}

	// Generates a random point/location in the square of Athens
	
	private Point getRandomLocation() {
		return new Point(Math.random()%0.1+23.6825 , Math.random()%0.1+37.9545);
	}
	
	public Enumeration <FingerEntry> getNeighbors() {
		return this.ft.getFingerEntries().elements();
	}
	
	/*
	 * NOT TESTED BUT PSHOULD WORK. NO FINGER ENTRIES TO TEST IT.
	 * Return true if a node exists in the convex hull of the triangulation.
	 * In other words if it is an outer node.
	 */
	
	public boolean inConvexHull() {
		if(this.ft.getFingerEntries().size() <2)
			return false;
		Enumeration <FingerEntry> e = this.ft.getFingerEntries().elements();
		FingerEntry first = e.nextElement();
		while(e.hasMoreElements()) {
			FingerEntry next = e.nextElement();
			if(!Orientation.isCounterClockwise(this.getLocation().getX(), this.getLocation().getY(), first.getPoint().getX(), first.getPoint().getY(), next.getPoint().getX(), next.getPoint().getY()))
				return true;
			first = next;
		}
		if(!Orientation.isCounterClockwise(this.getLocation().getX(), this.getLocation().getY(), this.ft.getFingerEntries().elementAt(this.ft.getFingerEntries().size()-1).getPoint().getX(), this.ft.getFingerEntries().elementAt(this.ft.getFingerEntries().size()-1).getPoint().getY(), this.ft.getFingerEntries().elementAt(0).getPoint().getX(), this.ft.getFingerEntries().elementAt(0).getPoint().getY()))
			return true;
		return false;
	}
}